<?php
/**
 * 验证码生成类
 * @author: langming
 * @date: 2021/8/24 上午10:31
 */

namespace Langming\Captcha;

use Exception;

class CaptchaService
{

    // 验证码实例
    private $im = null;

    // 验证码渲染实例
    private $draw = null;

    // 验证码颜色
    protected $color = '';

    // 验证码字符集合
    protected $codeSet = '2345678abcdefhjkmnpqrtuvwxyABCDEFHJKMNPQRTUVWXY';

    // 使用中文验证码
    protected $useZh = false;

    // 中文验证码字符串
    protected $zhSet = '天地玄黄宇宙洪荒日月列张寒来暑往秋收冬闰余成岁律吕调阳云腾致雨结为金生丽水玉出昆冈剑号巨珠称夜光果珍李重姜海咸河淡羽翔龙师火帝鸟官人皇始制文字乃服衣裳推位让国有唐吊民伐罪周发汤坐朝问道垂拱平章爱育';

    // 算术验证码
    protected $math = false;

    // 算术验证码字符集合
    protected $mathSet = '0123456789+-*/=';

    // 随机运算符号，支持加法(+)、减法(-)、乘法(*)、除法(/)四则运算，默认执行加法运算
    protected $operators = ['+', '-', '*', '/'];

    // 验证码字体大小(px)
    protected $fontSize = 25;

    // 是否画混淆曲线
    protected $useCurve = false;

    // 是否添加杂点
    protected $useNoise = false;

    // 杂点大小
    protected $fontSizeNoise = 20;

    // 验证码图片高度
    protected $imageH = 0;

    // 验证码图片宽度
    protected $imageW = 0;

    // 验证码位数
    protected $length = 4;

    // 验证码字体，不设置随机获取
    protected $fontFamily = '';

    // 背景颜色
    protected $bg = '';

    /**
     * 配置验证码
     * @param array|null $config
     * @return void
     */
    protected function configure(array $config): void
    {
        foreach ($config as $key => $val) {
            if (property_exists($this, $key)) {
                $this->{$key} = $val;
            }
        }
    }

    /**
     * 创建验证码
     * @return array
     * @throws Exception
     */
    protected function generate(): array
    {
        $bag = '';
        if ($this->math) {
            $y = random_int(1, 9);
            switch ($this->operators ? $this->operators[array_rand($this->operators)] : '+') {
                case '-':
                    $x = random_int(10, 30);
                    $bag = "{$x}-{$y}=";
                    $key = $x - $y;
                    break;
                case '*':
                    $x = random_int(1, 10);
                    $bag = "{$x}*{$y}=";
                    $key = $x * $y;
                    break;
                case '/':
                    $x = mt_rand(1, 10) * $y;
                    $bag = "{$x}/{$y}=";
                    $key = $x / $y;
                    break;
                default:
                    $x = random_int(10, 30);
                    $bag = "{$x}+{$y}=";
                    $key = $x + $y;
                    break;
            }
            $key .= '';
        } else {
            if ($this->useZh) {
                $characters = preg_split('/(?<!^)(?!$)/u', $this->zhSet);
            } else {
                $characters = str_split($this->codeSet);
            }
            for ($i = 0; $i < $this->length; $i++) {
                $bag .= $characters[rand(0, count($characters) - 1)];
            }
            $key = mb_strtolower($bag, 'UTF-8');
        }

        return [
            'value' => $key,
            'bag' => $bag,
        ];
    }

    /**
     * 创建验证码
     * @param array|null $config
     * @return CaptchaResult
     * @throws \ImagickException
     */
    public function create(array $config): CaptchaResult
    {
        $this->configure($config);
        if ($this->math) {
            $this->length = 5;
        }

        // 图片宽(px)
        $this->imageW || $this->imageW = $this->length * $this->fontSize * 1.5 + $this->length * $this->fontSize / 2;

        // 图片高(px)
        $this->imageH || $this->imageH = $this->fontSize * 2.5;

        $this->im = new \Imagick();

        // 建立一幅大小为 $this->imageW * $this->imageH 的画布，背景色为 $this->bg
        $this->im->newImage($this->imageW, $this->imageH, $this->bg ? $this->bg : 'rgba(243, 251, 254, 1)');

        $this->draw = new \ImagickDraw();

        // 验证码使用随机字体
        $fontPath = __DIR__ . '/../assets/';
        if ($this->useZh) {
            $fontPath .= '/fonts@zh/';
        } elseif ($this->math) {
            $fontPath .= '/fonts@math/';
        } else {
            $fontPath .= '/fonts@default/';
        }

        if (empty($this->fontFamily)) {
            $dir = dir($fontPath);
            $fontArray = [];
            while (false !== ($file = $dir->read())) {
                if ('.' != $file[0] && preg_match("/^\.(ttf|ttc|otc)$/i", substr($file, -4))) {
                    $fontArray[] = $file;
                }
            }
            $dir->close();
            $this->fontFamily = $fontArray[array_rand($fontArray)];
        }

        // 字体全路径
        $fontttf = $fontPath . $this->fontFamily;

        // 设置验证码字体
        $this->draw->setFont($fontttf);

        // 设置验证码字体大小
        $this->draw->setFontSize($this->fontSize);

        // 获取创建好的验证码
        $generator = $this->generate();

        // 将验证码从字符串转成数组
        $text = $this->useZh ? preg_split('/(?<!^)(?!$)/u', $generator['bag']) : str_split($generator['bag']);

        // 将验证码字符，挨个画出来
        $margin = 5;
        $width = $this->imageW - $margin * 2;
        $itemW = intval($width / $this->length);

        foreach ($text as $index => $char) {
            $start = $margin + $itemW * $index;
            $end = $margin + $itemW * ($index + 1);
            $mid = $start + intval(($end - $start) / 2);

            $x = mt_rand($start, $mid + mt_rand(-5, 5));
            if ($this->math) {
                $x = $start;
            }

            $y = $this->fontSize + mt_rand(10, 20);
            $angle = $this->math ? 0 : mt_rand(-20, 20);

            // 验证码文字以及坐标
            $this->draw->annotation($x, $y, $char);
            // 验证码文字在x轴上的倾斜角度
            $this->draw->skewX($angle);
            // 验证码文字颜色
            if (!$this->color) {
                $this->draw->setFillColor(
                    'rgb(' .
                    mt_rand(1, 150)
                    . ',' .
                    mt_rand(1, 150)
                    . ',' .
                    mt_rand(1, 150)
                    . ')'
                );
            } else {
                $this->draw->setFillColor($this->color);
            }
        }

        if ($this->useCurve) {
            // 绘干扰线
            $this->writeCurve();
        }

        if ($this->useNoise) {
            // 绘杂点
            $this->writeNoise();
        }

        // 验证码输出格式
        $this->im->setImageFormat("png");

        // 验证码最终输出的样式
        $this->im->drawImage($this->draw);

        // 输出图像
        ob_start();
        echo $this->im;
        $content = ob_get_clean();

        // 销毁imagick对象
        $this->im->destroy();
        return new CaptchaResult($content, $generator, 'image/png', $config);
    }

    /**
     * 画一条由两条连在一起构成的随机正弦函数曲线作干扰线(你可以改成更帅的曲线函数)
     * 正弦型函数解析式：y=Asin(ωx+φ)+b
     * 各常数值对函数图像的影响：
     * A：决定峰值（即纵向拉伸压缩的倍数）
     * b：表示波形在Y轴的位置关系或纵向移动距离（上加下减）
     * φ：决定波形与X轴位置关系或横向移动距离（左加右减）
     * ω：决定周期（最小正周期T=2π/∣ω∣）
     */
    protected function writeCurve(): void
    {
        // imagick 画贝塞尔曲线
        $px = $py = 0;
        // 文本随机颜色（验证码颜色）
        $this->draw->setFillColor(
            'rgb(' .
            mt_rand(150, 225)
            . ',' .
            mt_rand(150, 225)
            . ',' .
            mt_rand(150, 225)
            . ')'
        );

        // 曲线前部分
        $A = mt_rand(1, intval($this->imageH / 2)); // 振幅
        $b = mt_rand(-intval($this->imageH / 4), intval($this->imageH / 4)); // Y轴方向偏移量
        $f = mt_rand(-intval($this->imageH / 4), intval($this->imageH / 4)); // X轴方向偏移量
        $T = mt_rand($this->imageH, $this->imageW * 2); // 周期
        $w = (2 * M_PI) / $T;

        $px1 = 0; // 曲线横坐标起始位置
        $px2 = mt_rand(intval($this->imageW / 2), intval($this->imageW * 0.8)); // 曲线横坐标结束位置

        for ($px = $px1; $px <= $px2; $px = $px + 1) {
            if (0 != $w) {
                $py = $A * sin($w * $px + $f) + $b + $this->imageH / 2; // y = Asin(ωx+φ) + b
                $i = (int)($this->fontSize / 5);
                $coord = [];    // 坐标
                while ($i > 0) {
                    $coord[] = ['x' => $px + $i, 'y' => $py + $i];
                    $i--;
                }
                $this->draw->bezier($coord);
            }
        }

        // 曲线后部分
        $A = mt_rand(1, intval($this->imageH / 2)); // 振幅
        $f = mt_rand(-intval($this->imageH / 4), intval($this->imageH / 4)); // X轴方向偏移量
        $T = mt_rand($this->imageH, $this->imageW * 2); // 周期
        $w = (2 * M_PI) / $T;
        $b = $py - $A * sin($w * $px + $f) - $this->imageH / 2;
        $px1 = $px2;
        $px2 = $this->imageW;

        for ($px = $px1; $px <= $px2; $px = $px + 1) {
            if (0 != $w) {
                $py = $A * sin($w * $px + $f) + $b + $this->imageH / 2; // y = Asin(ωx+φ) + b
                $i = (int)($this->fontSize / 5);
                $coord = [];    // 坐标
                while ($i > 0) {
                    $coord[] = ['x' => $px + $i, 'y' => $py + $i];
                    $i--;
                }
                $this->draw->bezier($coord);
            }
        }
    }

    /**
     * 画杂点
     * 往图片上写不同颜色的文字
     */
    protected function writeNoise(): void
    {
        $bag = '';
        if ($this->math) {
            $characters = str_split($this->mathSet);
        } elseif ($this->useZh) {
            $characters = preg_split('/(?<!^)(?!$)/u', $this->zhSet);
        } else {
            $characters = str_split($this->codeSet);
        }

        for ($i = 0; $i < 10; $i++) {
            $bag .= $characters[rand(0, count($characters) - 1)];
        }

        $key = mb_strtolower($bag, 'UTF-8');

        $text = $this->useZh ? preg_split('/(?<!^)(?!$)/u', $key) : str_split($key);

        foreach ($text as $index => $char) {
            // 文本字体大小
            $this->draw->setFontSize($this->fontSizeNoise);
            // 文本随机颜色（验证码颜色）
            $this->draw->setFillColor(
                'rgba(' .
                mt_rand(150, 225)
                . ',' .
                mt_rand(150, 225)
                . ',' .
                mt_rand(150, 225)
                . ',' .
                mt_rand(3, 5) / 10
                . ')'
            );
            // 图片上插入随机文本（验证码）
            $this->draw->annotation(mt_rand(-10, $this->imageW), mt_rand(-10, $this->imageH), $char);
        }
    }
}
